package com.xingchi.tornado.storage.core.supports;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.CompleteMultipartUploadRequest;
import com.aliyun.oss.model.InitiateMultipartUploadRequest;
import com.aliyun.oss.model.InitiateMultipartUploadResult;
import com.aliyun.oss.model.OSSObject;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PartETag;
import com.aliyun.oss.model.UploadPartRequest;
import com.aliyun.oss.model.UploadPartResult;
import com.xingchi.tornado.basic.ErrorCode;
import com.xingchi.tornado.exception.BizException;
import com.xingchi.tornado.storage.core.AbstractFileStorageService;
import com.xingchi.tornado.storage.core.config.StorageProperties;
import com.xingchi.tornado.storage.core.exception.FileUploadException;
import com.xingchi.tornado.storage.core.model.dto.FileChunkMergeDTO;
import com.xingchi.tornado.storage.core.model.dto.FileChunkUploadDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 阿里云oss文件存储服务
 *
 * @author xingchi
 * @date 2024/4/30 0:04
 */
@Slf4j
public class OSSFileStorageService extends AbstractFileStorageService {

    private final String bucketName;
    private final OSS oss;
    private final StorageProperties.OSSProperties ossProperties;

    public OSSFileStorageService(StorageProperties.OSSProperties ossProperties) {
        // oss属性校验
        this.ossCheck(ossProperties);

        this.bucketName = ossProperties.getBucketName();
        this.ossProperties = ossProperties;
        this.oss = new OSSClientBuilder().build(ossProperties.getEndpoint(), ossProperties.getAccessKey(), ossProperties.getSecretKey());

        if (ossProperties.isAuto() && !oss.doesBucketExist(bucketName)) {
            oss.createBucket(bucketName);
            log.info("bucket not exists auto create bucket: '{}'", bucketName);
        } else if (!oss.doesBucketExist(bucketName)) {
            BizException.throwOf(ErrorCode.PARAM_IS_BLANK, "bucket not exists, please create bucket first");
        }
        log.info("aliyun oss file storage service init successfully!");
    }

    private void ossCheck(StorageProperties.OSSProperties ossProperties) {

        BizException.throwIf(StringUtils.isBlank(ossProperties.getEndpoint()), ErrorCode.ALIYUN_OSS_CLIENT_LOAD_FAIL, "aliyun oss endpoint is blank!");
        BizException.throwIf(StringUtils.isBlank(ossProperties.getAccessKey()) || StringUtils.isBlank(ossProperties.getSecretKey()), ErrorCode.ALIYUN_OSS_CLIENT_LOAD_FAIL, "aliyun oss accessKey or secretKey is empty!");
        BizException.throwIf(StringUtils.isBlank(ossProperties.getBucketName()), ErrorCode.ALIYUN_OSS_CLIENT_LOAD_FAIL, "aliyun oss bucketName is empty!");

    }

    private String replacePath(String path) {
        if (path.startsWith("/")) {
            path = path.substring(1);
        }
        return path;
    }

    /**
     * 根据文件的输入流上传文件
     *
     * @param is       文件的输入流
     * @param fileName 文件的完整名称
     * @return 文件在文件存储服务器上的路径唯一标识，在云存储中返回文件的对象名
     */
    @Override
    public String uploadFile(InputStream is, String fileName) {
        String path = getPath(fileName);

        try {
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentDisposition("attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20"));
            oss.putObject(bucketName, replacePath(path), is, metadata);
        } catch (OSSException | ClientException | UnsupportedEncodingException e) {
            log.error("阿里云OSS上传文件失败，原因：{}", e.getMessage(), e);
            BizException.throwOf(ErrorCode.FILE_UPLOAD_FAILED, e.getMessage());
        }

        return path;
    }

    /**
     * 根据文件在文件服务器上的存储路径获取文件对应的字节书，注意文件过大可能会导致内存溢出问题
     *
     * @param path 文件的存储路径
     * @return 文件字节
     */
    @Override
    public byte[] getFileBytes(String path) {
        try {
            return IOUtils.toByteArray(this.getInputStream(replacePath(path)));
        } catch (IOException e) {
            log.warn("get file bytes error: {}", e.getMessage());
            return new byte[0];
        }
    }

    @Override
    public InputStream getInputStream(String path){
        OSSObject object = oss.getObject(bucketName, replacePath(path));
        return object.getObjectContent();
    }

    /**
     * 删除文件
     *
     * @param path 文件的存储路径
     */
    @Override
    public void deleteFile(String path) {
        oss.deleteObject(bucketName, replacePath(path));
    }

    /**
     * 获取文件的访问url
     *
     * @param path 文件的存储路径，在云存储中则为对应的对象名称
     * @return url
     */
    @Override
    public String getUrl(String path) {
        Duration duration = ossProperties.getExpireTime();
        long millis = duration.toMillis();
        Date expireTime = new Date(System.currentTimeMillis() + millis);
        URL url = oss.generatePresignedUrl(bucketName, replacePath(path), expireTime);
        return url.toString();
    }

    @Override
    protected boolean isChunkExists(String chunkPath) {
        try {
            oss.getObjectMetadata(bucketName, replacePath(chunkPath));
            return true;
        } catch (OSSException e) {
            return false;
        }
    }

    @Override
    protected void storeChunk(FileChunkUploadDTO info, String chunkPath) {
        try {
            oss.putObject(bucketName, replacePath(chunkPath),
                    new ByteArrayInputStream(info.getContent()));
        } catch (Exception e) {
            log.error("分片上传失败，原因：{}", e.getMessage(), e);

            throw new FileUploadException("分片上传失败");
        }
    }

    @Override
    protected String combineChunks(FileChunkMergeDTO info, String targetPath) {
        try {
            List<PartETag> partETags = new ArrayList<>();
            String chunkPath = getChunkPath(info.getSha256());

            // 初始化分片上传
            InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(
                    bucketName, replacePath(targetPath));
            InitiateMultipartUploadResult initResult = oss.initiateMultipartUpload(initRequest);
            String uploadId = initResult.getUploadId();

            // 上传分片
            for (int i = 1; i <= info.getTotalChunks(); i++) {
                String chunk = chunkPath + i + ".chunk";
                OSSObject ossChunk = oss.getObject(bucketName, replacePath(chunk));

                UploadPartRequest uploadPartRequest = new UploadPartRequest();
                uploadPartRequest.setBucketName(bucketName);
                uploadPartRequest.setKey(replacePath(targetPath));
                uploadPartRequest.setUploadId(uploadId);
                uploadPartRequest.setInputStream(ossChunk.getObjectContent());
                uploadPartRequest.setPartSize(ossChunk.getObjectMetadata().getContentLength());
                uploadPartRequest.setPartNumber(i);

                UploadPartResult uploadPartResult = oss.uploadPart(uploadPartRequest);
                partETags.add(uploadPartResult.getPartETag());
            }

            // 完成分片上传
            CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
                    bucketName, replacePath(targetPath), uploadId, partETags);
            oss.completeMultipartUpload(completeRequest);

            // 清理分片
            for (int i = 1; i <= info.getTotalChunks(); i++) {
                oss.deleteObject(bucketName, replacePath(chunkPath + i + ".chunk"));
            }

            return targetPath;
        } catch (Exception e) {
            log.error("分片合并文件失败，原因：{}", e.getMessage(), e);
            throw new FileUploadException("分片合并失败");
        }
    }

}
